查看原文
其他

VC黑防日记:DLL隐藏和逆向(续)

小迪xiaodi 看雪学院 2021-03-07
本文为看雪论坛优秀文章
看雪论坛作者ID:小迪xiaodi 


【实验平台】:Win7 x64
【开发平台】:Win10 x64 + VS2017
【调试工具】:Ollydbg

经过几天的努力,我的毕业论文终于写的差不多了,老师说:


OK,那咱们继续回论坛吹水~

前段日子呢,写了个《VC黑防日记(一):DLL隐藏和逆向(上)》的水文,也得到论坛的大佬对我“能力”的认可:


那我们今天,继续来混一下字数,谈谈上次遗留的的问题——干掉吾爱破解OD这类工具对HOOK FreeLibrary方式隐藏DLL的检测。

怎么干呢,很简单,就是让它没了。

注入进去DLL之后,拷贝DLL镜像到一块内存地址,然后卸载DLL后,再把内存镜像还原回原地址。

这样子,只是简单的内存数据复制,而DLL的确被卸载了,拷贝后的DLL内存数据,只要计算下导出函数地址便可以调用,相当于远程调Call。

在此,也要感谢上次各位老哥推荐的灵活的方法思路,本文章并未进行复现,有机会可以一块儿写出来,在此表示感谢!




0x01 回顾



我们上次是通过分析FreeLibrary函数的流程得知大概分为四个步骤:
1. 判断DLL句柄是否有效,有效就说明该DLL存在于进程中
2. 递减模块的引用计数,且判断是否为0
3. 调用模块的DllMain函数响应 DLL_PROCESS_DETACH消息
4. 从进程空间撤销对DLL的内存映射

然后我们通过对第四个步骤的分析,采用HOOK  FreeLibrary 函数中的 ZwUnmapViewOfSection函数,实现了擦除DLL痕迹隐藏DLL的功能,但是出现了一丢丢的问题,那就是被某些“强大”的工具检测并枚举了出来,那么为什么会这样呢?今天我们就来分析分析~



0x02 分析



既然我们上次HOOK  ZwUnmapViewOfSection出现了小问题,能不能对 ZwUnmapViewOfSection做一些小手脚呢,暂时我的知识储备还不够,处理不了,但是我选择了其他的绕过方式,在讲这个方式之前,我们先了解一部分逆向的理论知识。

虚拟地址描述符:
大家都知道,在x86的平台上Windows操作系统为每个进程描述了一个完整的4G的地址空间,这4G空间由低位2G的用户地址空间和高位2G的系统地址空间构成。每个进程的用户地址空间是相互隔离的,不可见的。但是系统地址空间是各个进程间共享的,对于进程有不同的视图。用户的私有的数据代码还有加载的动态链接库(DLL)都存放在用户地址空间中。现在有个问题是,总要有个地方记录着这2G地址空间,到底那些被预留了,那些被提交了,那些被访问了吧。还有个问题就是,对于程序来讲,地址不是连续的,是分段的。代码段、数据段、堆、栈等等。可是进程的空间是连续的,从0x0000000到0x7FFFFFF。总要有个数据结构描述程序的各个段对应那段地址空间。这两个重要任务就交个了VAD,即虚拟地址描述符(Virtual Address Descriptor)。 VAD组织成了一个AVL自平衡二叉树(参考Mark Russinovich的《深入解析Windows操作系统》),这种组织方式完全是方便快速查找。树中的每一个节点代表了一段虚拟地址空间。所以程序的代码段,数据段,堆段都会各种占用一个或多个VAD节点,由一个MMVAD结构完整描述。内核空间并不受VAD的管理。 https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80%E6%8F%8F%E8%BF%B0%E7%AC%A6/295257?fr=aladdin

分析一下:
1.有个玩意儿叫做“虚拟地址描述符”,存放了DLL的内存空间使用情况数据结构。
2.如果正常调用 ZwUnmapViewOfSection,内存地址所对应的“VAD”就会从树中摘除。
3.我们上次hook了 ZwUnmapViewOfSection,刚好使得被注入的DLL的 “内存空间数据结构”无法从整体的“二叉树”结构中消除。

结论:
1. 如果不HOOK ZwUnmapViewOfSection ,就假戏真做,DLL就真的被卸载了。
2. 如果HOOK了  ZwUnmapViewOfSection,就很难受,虚拟地址描述符清除不掉,还是过不了吾爱破解OD的检测。
3. 我们需要找一个方法在假戏真做的同时,保存DLL的内存镜像。




0x03 解决方案—内存拷贝法



由上一节的分析我们得知,如果我们假戏真做,DLL会被完整的卸载掉,那么我们如果在卸载之前先保存自身DLL内存镜像到另外的位置,然后真正的卸载掉,最后再把内存镜像拷贝回去就可以了,拷贝的时候拷贝回原地址是最好的方法,因此,我们开始解决问题吧!

1. 拷贝DLL内存镜像需要得知内存镜像的大小,根据PE文件结构操作,解:
char szExePath[MAX_PATH] = "C:\\Users\\86186\\Desktop\\mydll.dll"; HANDLE hFile = CreateFile(szExePath, GENERIC_ALL, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, NULL, NULL); //获得PE文件句柄 HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL); //创建一个新的文件映射内核对象 PVOID pbFile = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0); //将一个文件映射对象映射到内存,得到指向映射到内存的第一个字节的指针pbFile if (INVALID_HANDLE_VALUE == hFile || NULL == hMapping || NULL == pbFile) { printf("\n\t---------- The File Inexistence! ----------\n"); if (NULL != pbFile) { UnmapViewOfFile(pbFile); } if (NULL != hMapping) { CloseHandle(hMapping); } if (INVALID_HANDLE_VALUE != hFile) { CloseHandle(hFile); } return 0; } //pDosHeader指向DOS头起始位置 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pbFile; printf("PE Header e_lfanew:0x%x\n", pDosHeader->e_lfanew); //计算PE头位置 PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pbFile + pDosHeader->e_lfanew); //计算DLL模块镜像大小 DWORD dwSizeOfImage = (DWORD)pNTHeader->OptionalHeader.SizeOfImage; printf("SizeOfImage: 0x%08X\n", dwSizeOfImage);

得到DLL模块镜像大小 == 我们要复制写的内存的大小。

2. 分配内存地址方便写入

使用VirtualAllocEx函数: https://docs.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex
VirtualAllocEx(hProcess, NULL, dwSizeOfImage, MEM_COMMIT, PAGE_READWRITE);

然后修改内存属性为可读可写:
BOOL VirtualProtectEx( HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);

3. 内存拷贝DLL数据

//保存数组BYTE code[] = { 0 };DWORD lp_copy = (DWORD)lpaddress;DWORD lp_start = addr_start; for (int i = 0; i < dwSizeOfImage; i++, lp_start++, lp_copy++) { ReadProcessMemory(hProcess, (LPCVOID)lp_start, code, 1, NULL); WriteProcessMemory(hProcess, (LPVOID)lp_copy, code, 1, NULL);}printf("原地址 = 0x%x 拷贝地址 = 0x%x\n", addr_start, lpaddress);MessageBox(NULL, "拷贝完成!", "Cap", MB_OK);

4. 卸载DLL

卸载DLL后就可以真正的无影无踪了:
UnInject(Pid, DLL路径);

5. 这样,DLL就真的没了,被拷贝在别的位置,只要知道起始地址,那么就可以正常的调用DLL的导出函数了。


本次实验的代码如下:
int main(){ const char* a = "C:\\Users\\86186\\Desktop\\mydll.dll"; HANDLE hToken = NULL; //打开当前进程的访问令牌 int hRet = OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken); if (hRet) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; //取得描述权限的LUID LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid); tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; //调整访问令牌的权限 AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL); CloseHandle(hToken); } char szExePath[MAX_PATH] = "C:\\Users\\86186\\Desktop\\mydll.dll"; HANDLE hFile = CreateFile(szExePath, GENERIC_ALL, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, NULL, NULL); //获得PE文件句柄 HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL); //创建一个新的文件映射内核对象 PVOID pbFile = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0); //将一个文件映射对象映射到内存,得到指向映射到内存的第一个字节的指针pbFile if (INVALID_HANDLE_VALUE == hFile || NULL == hMapping || NULL == pbFile) { printf("\n\t---------- The File Inexistence! ----------\n"); if (NULL != pbFile) { UnmapViewOfFile(pbFile); } if (NULL != hMapping) { CloseHandle(hMapping); } if (INVALID_HANDLE_VALUE != hFile) { CloseHandle(hFile); } return 0; } //pDosHeader指向DOS头起始位置 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pbFile; printf("PE Header e_lfanew:0x%x\n", pDosHeader->e_lfanew); //计算PE头位置 PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pbFile + pDosHeader->e_lfanew); //计算DLL模块镜像大小 DWORD dwSizeOfImage = (DWORD)pNTHeader->OptionalHeader.SizeOfImage; printf("SizeOfImage: 0x%08X\n", dwSizeOfImage); UnmapViewOfFile(pbFile); CloseHandle(hMapping); CloseHandle(hFile); Inject(GetProcessIDByName("代码注入器.exe"), (char*)a); MessageBox(NULL, "注入完成!", "Cap", MB_OK); int Pid = 0; while (1) { Pid = GetProcessIDByName("代码注入器.exe"); if (Pid > 0) { MessageBox(NULL, "检测到进程,点击确定开始申请内存!", "Cap", MB_OK); break; } } //申请内存 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid); DWORD dwOldProtect; LPVOID lpaddress = VirtualAllocEx(hProcess, NULL, dwSizeOfImage, MEM_COMMIT, PAGE_READWRITE); printf("分配地址:0x%x\n",lpaddress); VirtualProtectEx(hProcess, lpaddress, dwSizeOfImage + 1, PAGE_EXECUTE_READWRITE, &dwOldProtect); DWORD addr_start = 0; while (1) { addr_start = GetProcessModuleHandleByName(Pid, "mydll.dll"); if (addr_start > 0) { MessageBox(NULL, "检测到DLL被注入了,点击确定开始拷贝DLL!", "Cap", MB_OK); break; } } //保存数组 BYTE code[] = { 0 }; DWORD lp_copy = (DWORD)lpaddress; DWORD lp_start = addr_start; for (int i = 0; i < dwSizeOfImage; i++, lp_start++, lp_copy++) { ReadProcessMemory(hProcess, (LPCVOID)lp_start, code, 1, NULL); WriteProcessMemory(hProcess, (LPVOID)lp_copy, code, 1, NULL); } printf("原地址 = 0x%x 拷贝地址 = 0x%x\n", addr_start, lpaddress); MessageBox(NULL, "拷贝完成!", "Cap", MB_OK); //真正去卸载 UnInject(GetProcessIDByName("代码注入器.exe"), (char*)a); printf("原地址 = 0x%x \n", addr_start); //还原DLL镜像至原地址 DWORD VirtualAddress = lpaddress; VirtualFreeEx(hProcess, addr_start, dwSizeOfImage + 1, MEM_RELEASE); DWORD returnValue = VirtualAllocEx(hProcess, addr_start, dwSizeOfImage + 1, MEM_COMMIT, PAGE_READWRITE); int error1 = GetLastError(); printf("the value = %d error = %d \n", returnValue, error1); MessageBox(NULL, "原内存地址恢复完成!", "Cap", MB_OK); VirtualProtectEx(hProcess, addr_start, dwSizeOfImage + 1, PAGE_EXECUTE_READWRITE, &dwOldProtect); for (int i = 0; i < dwSizeOfImage; i++, addr_start++, VirtualAddress++) { ReadProcessMemory(hProcess, (LPCVOID)VirtualAddress, code, 1, NULL); WriteProcessMemory(hProcess, (LPVOID)addr_start, code, 1, NULL); } MessageBox(NULL, "还原完成!DLL隐藏完成!", "Cap", MB_OK); getchar(); return 0;}


0x04 未解决不完美之处



本来打算把DLL的内存镜像复制回卸载DLL之前DLL在程序中的内存地址的,无奈分配地址会出错,暂时还没找到解决的方法。

如果能够复制回原来的地址就比较完美了。

DWORD returnValue = VirtualAllocEx(hProcess, addr_start, dwSizeOfImage + 1, MEM_COMMIT, PAGE_READWRITE); int error1 = GetLastError(); printf("the value = %d error = %d \n", returnValue, error1); MessageBox(NULL, "原内存地址恢复完成!", "Cap", MB_OK);

通过GetLastError得到结果:



发现错误487来源于:



暂时还没解“初始化的安全区”导致的分配原地址失败的问题,如果有大佬会的话希望大佬能够评论一下~

结语

把一些新奇的想法用代码去做实验实现还是比较有意思的,技术有些粗陋,重要的是享受过程(其实就是我菜)




- End -





看雪ID:小迪xiaodi

https://bbs.pediy.com/user-680946.htm 

*本文由看雪论坛 小迪xiaodi 原创,转载请注明来自看雪社区。




推荐文章++++

攻防世界fakebook关卡攻略

Win32 Shellcode编写

**游戏逆向分析笔记

对宝马车载apps协议的逆向分析研究

x86_64架构下的函数调用及栈帧原理


好书推荐





公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存